Skip to content

Scaffold Swift-implemented drop-in replacement for libbluetooth.so.3 - #63

Open
colemancda wants to merge 73 commits into
masterfrom
feature/c-abi
Open

Scaffold Swift-implemented drop-in replacement for libbluetooth.so.3#63
colemancda wants to merge 73 commits into
masterfrom
feature/c-abi

Conversation

@colemancda

@colemancda colemancda commented Aug 1, 2026

Copy link
Copy Markdown
Member

Summary

Scaffolding for a drop-in, Swift-implemented replacement for BlueZ's shared libbluetooth.so.3. The deliverable is a library that loads, resolves all 218 symbols the reference exports, and fails loudly (by symbol name) on any call not implemented yet — so the rest of the port can proceed incrementally, one symbol family at a time.

Nothing in the bluez package actually links this shared library — bluetoothd, bluetoothctl, btmon, hciconfig and sdptool all statically link libbluetooth-internal.a — so replacing it can't break the Bluetooth stack itself; it exists solely for third-party consumers.

  • Sources/CBluetoothLinuxABI vendors the eleven public BlueZ headers verbatim (GPL-2.0-or-later, isolated in its own directory with its own LICENSE and README) and generates a stub for every symbol not yet implemented, via scripts/gen_stubs.py. Each stub's signature is read out of the vendored headers, so it's checked against the real declaration rather than hand-typed.
  • scripts/symbols.txt records the reference export surface (218, split 17/101/100 across bluetooth.c/hci.c/sdp.c); scripts/implemented.txt records the 203 currently implemented: the bluetooth.c + bt_uuid_* families and the SDP data/list/record/codec layer (both implemented in PureSwift/Bluetooth), plus this repo's own HCI string converter family, HCI device management family, and the 71-symbol HCI command wrapper family (hci_inquiry, hci_create_connection, hci_read_*, hci_write_*, hci_le_*, etc.) — everything BlueZ's lib/hci.c exports except hci_strtoptype-adjacent string helpers already covered above. scripts/gen_symbols.py derives the linker version script and the flat export list from these; scripts/check-exports.sh asserts the built library against it.
  • scripts/ownership.md records the return/who-frees/lifetime contract for each symbol, ahead of implementation — the conventions aren't uniform (some allocate on the heap, some write into a caller buffer, some return static pointers) and guessing produces leaks or double frees no type checker would catch.
  • Sources/BluetoothLinuxABI implements the 21-symbol HCI string converter family, using generated lookup tables (scripts/generate-hci-tables.py, parsed directly from BlueZ's hci.c) for the bus/device-flag/packet-type/link-mode/version/command-name mappings; the 9-symbol HCI device management family (raw AF_BLUETOOTH/BTPROTO_HCI socket open/close, device info/address/id lookup, enumeration, routing, and sending commands/requests); and the 71-symbol HCI command wrapper family (connection/link-policy management, remote-device queries, local controller info, controller configuration, LE commands, and inquiry) built on hciCommand/hciStatus/hciRequest helpers that capture the send/check-status/copy-out shape every wrapper in lib/hci.c repeats. All of it talks to the kernel directly via socket/bind/ioctl/writev/poll rather than through BluetoothLinux's own async HostController/Socket infrastructure, since the ABI surface has to be synchronous and match the reference's wire layout precisely.
  • CMakeLists.txt builds libbluetooth.so.3.19.15 by linking a PureSwift/Bluetooth checkout's static archives (-DBLUETOOTH_PACKAGE_PATH) together with BluetoothLinuxABI and the generated stubs, with the soname, version, and a pinned export list that Package.swift can't express.
  • Conformance/compare.sh and Conformance/conformance_hci_strings.c differentially test the HCI string converter family against the system libbluetooth.so.3; the phase-1 and SDP drivers stay in the PureSwift/Bluetooth checkout alongside the symbols they cover.

Verified

  • The built library exports exactly the 218 expected symbols (check-exports target).
  • Its output over the differential conformance drivers for the symbols with a driver (bluetooth.c/bt_uuid_*, SDP, HCI strings) is identical to the system libbluetooth.so.3, aside from a handful of already-documented deltas (bt_compidtostr naming, one memcmp-magnitude case) recorded in each repo's known-differences.txt.
  • Calling an unimplemented symbol (e.g. hci_open_dev) aborts with libbluetooth (PureSwift): hci_open_dev is not implemented yet. rather than crashing unhelpfully or corrupting state.

Test plan

  • swift build — default configuration, unaffected
  • SWIFTPM_BLUETOOTH_CABI=1 swift build — C ABI target builds
  • SWIFTPM_BLUETOOTH_CABI=1 swift test — passes, including the new BluetoothLinuxABITests
  • cmake -B build -G Ninja -DBLUETOOTH_PACKAGE_PATH=<Bluetooth checkout> && cmake --build buildlibbluetooth.so.3.19.15 builds
  • cmake --build build --target check-exports — 218/218 symbols match
  • Conformance/compare.sh — identical output against the reference library for the HCI string converter driver
  • Smoke test: an unimplemented symbol aborts loudly by name instead of misbehaving

Known gap

hci_send_cmd/hci_send_req and everything built on them — the entire 71-symbol HCI command wrapper family, plus hci_inquiry's ioctl(HCIINQUIRY) call — have not been exercised against a real or virtual HCI device. Only their non-socket paths (input validation, hci_send_cmd's wire-format framing over a plain pipe) are unit tested. No /dev/vhci access was available (root-only in this environment) to build a differential driver for this family; each wrapper is a careful, field-by-field transcription of its lib/hci.c reference, cross-checked against the vendored struct layouts, but unverified end-to-end.

@colemancda colemancda changed the title Add libbluetooth.so.3 scaffolding (phase 0) Add C ABI Aug 1, 2026
colemancda and others added 30 commits August 2, 2026 18:43
Drops the 9 stubs now covered by BluetoothLinuxABI's device
management family (96 remaining, 132 implemented).
hciCommand/hciStatus/hciRequest capture the send/check-status/copy-out
shapes that every hci_send_req-based command wrapper in lib/hci.c
repeats, so each wrapper is a short, direct transcription of its
reference instead of duplicating the boilerplate.
hci_create_connection, hci_disconnect, hci_authenticate_link,
hci_encrypt_link, hci_change_link_key, hci_switch_role, hci_park_mode,
and hci_exit_park_mode.
hci_read_remote_name(_with_clock_offset), hci_read_remote_name_cancel,
hci_read_remote_version, hci_read_remote_features,
hci_read_remote_ext_features, and hci_read_clock_offset.
hci_read_local_version, hci_read_local_commands, hci_read_local_features,
hci_read_local_ext_features, hci_read_bd_addr, hci_read_local_name, and
hci_write_local_name.
White list and resolving list management, scan and advertising
control, and LE connection establishment/update/remote-features
query.
Class of device, voice setting, inquiry access codes, stored link
keys, inquiry/AFH/inquiry-mode toggles, extended inquiry response,
simple pairing, OOB data, transmit power, link policy/supervision
timeout, AFH classification, and per-connection link quality/RSSI/AFH
map/clock queries — 33 symbols.
Unlike the rest of the command wrapper family, this goes through a
single ioctl(HCIINQUIRY) rather than hci_send_req — the request header
and resulting inquiry_info records share one kernel buffer, the same
shape as HCIGETDEVLIST in HCIDevice.swift.
Drops the 71 stubs now covered by the HCI command wrapper family (25
remaining — the SDP session functions — 203 implemented).
PureSwift/Bluetooth 8.1.0 folds the 75 pure sdp_* symbols into
BluetoothABI rather than shipping them as a separate static library,
so there is no BluetoothSDP CMake target to name here any more. Its
symbols come in with BluetoothABI, which is already linked whole.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
Encoding a search pattern or an attribute ID list as a data element
sequence, and appending a continuation state. The reference keeps the
equivalents private to sdp.c and exports none of them, so they are
reimplemented here rather than linked against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
sdp_create, sdp_connect, sdp_close, sdp_get_socket, sdp_get_error,
sdp_gen_tid, sdp_set_notify and sdp_general_inquiry. sdp_session_t is
a non-opaque struct that some callers read directly, so its fields are
kept populated with real values rather than treated as opaque. The
private per-session state (the reference's struct sdp_transaction) is
not part of any vendored header and is reimplemented as SDPTransaction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
sdp_service_search_req, sdp_service_attr_req and
sdp_service_search_attr_req, built on the sdp_send_req_w4_rsp
send/wait-for-response primitive, following continuation state until
the response is complete.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
Registers the twelve socket-bound SDP client session functions so
gen_stubs.py stops emitting stubs for them, and retargets the pure
sdp_* note at PureSwift/Bluetooth's Sources/BluetoothABI, which is
where they live as of 8.1.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
13 stubs remaining, 215 implemented of 218 exported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
The SDP session sources import BluetoothABI for the PDU codec, which
the SwiftPM manifest already provides but the CMake build did not, so
libBluetoothLinuxABI.a failed to compile with "no such module". The
link has to be declared after add_subdirectory, which is where the
target is defined.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
Every multi-byte field in an SDP PDU sits wherever the preceding
fields leave it — the 5-byte header alone puts the body on an odd
address — so writing one through a typed pointer is not valid even
where x86 tolerates it. The registration and asynchronous request
builders use this instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
sdp_record_register/unregister/update, their sdp_device_record_*
forms, and the two _binary entry points underneath them.

The PDUs these exchange (0x75 through 0x80) are explicitly outside the
Bluetooth specification — the vendored sdp.h says so where it defines
them — so the wire layouts were recovered from the reference
libbluetooth.so.3, which is the only thing that documents them. Two of
its behaviours are reproduced rather than corrected because a client
can observe them: the response status is loaded in host order rather
than with ntohs, and sdp_device_record_update_binary is an exported
stub that returns -1 without sending anything.

Bounds that the reference delegates to _FORTIFY_SOURCE (where an
oversized record aborts the process) are checked here and reported as
EINVAL instead, which cannot break a caller that already worked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
sdp_service_search_async, sdp_service_attr_async and
sdp_service_search_attr_async only build and send; sdp_process does
everything about the reply — reassembling a response split across
continuation states, resending the request for each next fragment, and
finally invoking the sdp_set_notify callback.

sdp_process's return value is easy to misread: 0 means "not finished,
call me again when the socket is readable" and -1 means the
transaction is over and the callback has run. -1 is the normal ending
and says nothing about success.

The record-count accumulation in a fragmented search response adds two
big-endian values without swapping either, so a count that carries out
of the low byte comes out wrong. That is what the reference writes and
what a client reads back, so it is reproduced with a comment rather
than fixed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
Registers the nine record registration functions and the four
asynchronous ones, which leaves nothing stubbed: 228 implemented, 218
of them exported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
Empty: every exported symbol now has a real implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
The container was pinned to swift:6.0.2, which can no longer resolve
the package at all: PureSwift/Bluetooth 8.1.0 declares
swift-tools-version:6.2, and 6.0.2 rejects it with "incompatible tools
version" before compiling anything. The build and test steps now run
as a matrix over 6.2.3 and 6.3.3, debug and release.

Adds a job that builds and tests with SWIFTPM_BLUETOOTH_CABI=1 and
then runs the differential conformance drivers against the system
libbluetooth.so.3. Nothing in CI compiled a line of the C ABI before
this, so all 228 exported symbols were unguarded. That job is 6.3-only
because the boundary is bound with `@c`, which 6.2 rejects.

Coverage moves to its own job so it runs once rather than once per
matrix cell, which also keeps the uploaded artifact names unambiguous.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
SwiftPM builds the same sources but not the shipped artifact: soname,
symbol versioning and the export list are only expressible in
CMakeLists.txt, so nothing in CI was checking the thing that actually
gets installed. This builds it, runs check-exports — the assertion
that the library exports exactly the reference's symbol set, which is
what would catch the drop-in premise breaking — and installs it.

Needs a PureSwift/Bluetooth checkout, since the shared object links
the non-socket half of the C ABI out of that package's own CMakeLists.
It is pinned to the version Package.swift depends on so the two halves
cannot drift apart here.

noble rather than jammy: CMakeLists.txt requires CMake 3.26+ and
jammy's apt carries only 3.22.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
testClassicAddressDecoding expected a wire address type of 0 to decode
back to nil, but 0 is not an "unspecified" sentinel: it is the BR/EDR
address type, which is exactly why the encoder writes nil as 0. The
round trip is asymmetric by construction — nil encodes to 0, and 0
decodes to .bredr — so .bredr is the correct expectation, and the test
has been failing since it was added.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
The swift:6.3.3-noble container does not ship a python3, and configure
fails at find_package(Python3 REQUIRED) — the stub table and version
script are generated by scripts/*.py at build time. This never surfaced
locally because the host interpreter satisfied the search.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
The differential driver compares our output against whatever
libbluetooth3 the distro ships, and our tables match current BlueZ
(5.79, the Debian 13 reference this port is verified against). Jammy's
5.64 predates both bus type 11 (IPC) and HCI/LMP version 0x0d (5.4),
and noble's 5.72 still lacks the former, so CI prints the fallback
where we print the newer name.

Record those eight lines in known-differences.txt, the file
compare.sh already subtracts from the diff. Both sides of each pair
are listed because the subtraction is direction-blind; on a reference
that knows these values the outputs are identical and no entry
applies. Verified against jammy's actual .so (extracted from
ubuntu:22.04) via BT_REFERENCE_LIB + LD_LIBRARY_PATH: all 8 differing
lines accepted, and against the local 5.79 the output remains
byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
The header calls memset without declaring it, so clang implicitly
declares the builtin — `void *(void *, int, unsigned long)` — and that
declaration becomes part of the CBluetoothLinuxTest module, where it
shadows SwiftGlibc's annotated memset. Debug builds get away with it,
but in release, cross-module optimization deserializes
FoundationEssentials SIL that references SwiftGlibc.memset, resolution
finds only the mismatched builtin, and swift-frontend aborts with a
deserialization failure (signal 6) while compiling the test target.
This is what has been failing every release-configuration CI cell.

Verified: swift test -c release now builds and passes (60 tests, 0
failures) on 6.3.3, where it previously crashed the compiler.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
In a container job, ${{ github.workspace }} expands to the host runner
path (/home/runner/work/...), but the step executes inside the
container, where the workspace is mounted at /__w/... — so configure
looked for the Bluetooth checkout at a path that does not exist and
failed. The GITHUB_WORKSPACE environment variable is set by the runner
inside the container and resolves to the mounted path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nz7CmqG42Y9u2UJP1MawfW
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant